package com.terapico.caf;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.terapico.uccaf.CafEntity;
import org.springframework.beans.factory.BeanCreationException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SuppressWarnings("rawtypes")
public class ServletResultRenderer {

    public static final String X_HEADES = "X-Class, X-Redirect, X-Env-Type, X-Env-Name, X-Action, authorization, x-navigation-method";

    protected void renderMimeObjectResult(HttpServlet servlet, InvocationResult result, HttpServletRequest request,
                                          HttpServletResponse response) throws IOException {

        Object actualResult = result.getActualResult();
        if (!(actualResult instanceof BlobObject)) {
            throw new IllegalArgumentException("The return object is not a blob");
        }
        BlobObject blob = (BlobObject) actualResult;
        response.addHeader("X-Env-Type", result.getEnvType());
        response.addHeader("X-Env-Name", result.getEnvName());
        response.setCharacterEncoding(null);
        response.setContentType(blob.getMimeType());
        blob.getHeaders().forEach((k, v) -> response.setHeader(k, v));
        response.getOutputStream().write(blob.getData());

    }

    public void render(HttpServlet servlet, HttpServletRequest request, HttpServletResponse response,
                       InvocationResult result) throws ServletException, IOException {
        // When integrate with WeiXin, some special request has accept:*/* , and must
        // render as plainText
        // so decide render way by result.isRenderByXXX first
        if (result.getActualResult() instanceof BlobObject) {
            renderMimeObjectResult(servlet, result, request, response);
            return;
        }
        if (result.isAssignedRenderingWay()) {
            if (result.isRenderAsHtml()) {
                renderHTMLPage(servlet, result, request, response);
                return;
            }
            if (result.isRenderAsJavaScript()) {
                renderJavascript(servlet, result, request, response);
                return;
            }
            if (result.isRenderAsJSON()) {
                renderJson(result, request, response);
                return;
            }
            // otherwise, use default render-decide procedure
        }

        if (this.isRequestForHTML(request)) {
            renderHTMLPage(servlet, result, request, response);
            return;
        }
        if (this.isRequestForJavascript(request)) {
            renderJavascript(servlet, result, request, response);
            return;
        }

        if (ReflectionTool.isPrimaryType(result.getActualResult().getClass())) {
            renderAsString(result, request, response);
            return;
        }
        // Otherwise use JSON as the default return format
        renderJson(result, request, response);
        return;

    }

    private void renderAsString(InvocationResult result, HttpServletRequest request, HttpServletResponse response) throws IOException {

        response.setContentType("text/plain");

        String renderClass = result.getActualResult().getClass().getName();
        if (result.getResponseHeader() != null && result.getResponseHeader().containsKey("X-Class")) {
            renderClass = result.getResponseHeader().get("X-Class");
        }
        byte[] value = result.getActualResult().toString().getBytes();
        int length = value.length;
        fillOrigin(result, request, response);
        response.addHeader("X-Class", renderClass);
        response.addHeader("X-Env-Type", result.getEnvType());
        response.addHeader("X-Env-Name", result.getEnvName());
        response.addHeader("Content-Length", Long.valueOf(length).toString());
        response.addHeader("Access-Control-Expose-Headers", X_HEADES);

        response.getOutputStream().write(value);
        ;


    }

    protected String getHost(HttpServletRequest request) {
        if (!isBehideProxy(request)) {
            return request.getHeader("host");
        }
        return request.getHeader("X-Forwarded-Host");// include hostname and port
    }

    protected String getPort(HttpServletRequest request) {
        if (!isBehideProxy(request)) {
            return String.valueOf(request.getServerPort());
        }
        return request.getHeader("X-Forwarded-Port");
    }

    protected String calcBaseURL(HttpServletRequest request) {
        String port = getPort(request);
        String host = getHost(request);
        if (port == null || port.isEmpty() || port.equalsIgnoreCase("null") || port.equals("80")
                || port.equals("443")) {
            return "//" + host + request.getContextPath();
        }
        if (host.indexOf(':') > 0) {
            return "//" + host + request.getContextPath();
        }
        return "//" + host + ":" + port + request.getContextPath();
    }

    private boolean isBehideProxy(HttpServletRequest request) {

        if (request.getHeader("X-Forwarded-For") != null) {
            return true;
        }
        if (request.getHeader("X-Forwarded-Host") != null) {
            return true;
        }
        if (request.getHeader("X-Forwarded-Server") != null) {
            return true;
        }

        return false;
    }

    protected boolean isRequestForJson(HttpServletRequest request) {

        return hasHeaderWithValue(request, "Accept", "application/json");

    }

    protected boolean isRequestForHTML(HttpServletRequest request) {

        return hasHeaderWithValue(request, "Accept", "text/html");

    }

    protected boolean isRequestForJavascript(HttpServletRequest request) {

        return hasHeaderWithValue(request, "Accept", "application/javascript");

    }

    protected void assertNotEmptyStringArgument(String methodName, String parameterName, String paramterValue) {
        if (paramterValue == null) {
            throw new IllegalArgumentException(
                    "Method '" + methodName + "': parameter: '" + parameterName + "' should not be null.");
        }
        if (paramterValue.isEmpty()) {
            throw new IllegalArgumentException(
                    "Method '" + methodName + "': parameter: '" + parameterName + "' should not be emptry string.");
        }
    }

    protected boolean hasHeaderWithValue(HttpServletRequest request, String header, String value) {
        final String methodName = "hasHeaderWithValue(HttpServletRequest request, String header, String value)";

        assertNotEmptyStringArgument(methodName, "header", header);
        assertNotEmptyStringArgument(methodName, "value", value);

        String accept = request.getHeader(header);
        if (accept == null) {
            return false;
        }
        if (accept.isEmpty()) {
            return false;
        }
        if (accept.contains(value)) {
            return true;
        }
        return false;

    }

    private ObjectMapper objectMapper = null;

    protected ObjectMapper getObjectMapper() {
        //// objectMapper = null;
        // if(objectMapper == null){
        // objectMapper = new ObjectMapper();
        // return objectMapper;
        // //DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        // //objectMapper.setDateFormat(df);
        // }
        //
        // return objectMapper.copy();
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        return mapper;
    }

    protected void fillOrigin(InvocationResult result, HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("Origin");
        if (origin == null) {
            return;
        }
        response.addHeader("Access-Control-Allow-Origin", origin);
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
        // Access-Control-Expose-Headers
        response.addHeader("Access-Control-Expose-Headers", "Set-Cookie, X-Redirect, X-Env-Type, X-Env-Name, X-Action, authorization");
        response.addHeader("Access-Control-Allow-Credentials", "true");

    }

    protected String renderLogResult(Object value) {
        if (ReflectionTool.isPrimaryType(value.getClass())) {
            return value.toString();
        }
        return "<Object>";

    }

    protected void renderJson(InvocationResult result, HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // log();

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        String renderClass = result.getActualResult().getClass().getName();
        if (result.getResponseHeader() != null && result.getResponseHeader().containsKey("X-Class")) {
            renderClass = result.getResponseHeader().get("X-Class");
        }
        response.addHeader("X-Class", renderClass);
        response.addHeader("X-Env-Type", result.getEnvType());
        response.addHeader("X-Env-Name", result.getEnvName());
        response.addHeader("Access-Control-Expose-Headers", X_HEADES);
        // Access-Control-Expose-Headers
        String sessionId = request.getSession().getId();
        if (isHttps(request)) {
            response.setHeader("Set-Cookie", "JSESSIONID=" + sessionId + "; path=/; SameSite=None; Secure");
        } else {
            response.setHeader("Set-Cookie", "JSESSIONID=" + sessionId + "; path=/;");
        }

        log("Render JSON result with class: " + renderClass + "(" + renderLogResult(result.getActualResult()) + ")");
        // BeanCreationException

        // Access-Control-Allow-Origin
        fillOrigin(result, request, response);
        // Access-Control-Allow-Credentials: true
        // 其他header
        if (result.getResponseHeader() != null) {
            for (String hName : result.getResponseHeader().keySet()) {
                if ("X-Class".equals(hName)) {
                    continue;
                }
                response.addHeader(hName, result.getResponseHeader().get(hName));
            }
        }

        // Gson gson = new Gson();
        ObjectMapper mapper = getObjectMapper();
        // Type t=new TypeToken<weather.WeatherResponse>().getType();
        // response.getWriter().println(gson.toJson(result.getActualResult()));
        // mapper
        /*
         * Order
         * order=(Order)OrdreJsonTool.prepareForJson((Order)result.getActualResult());
         *
         */
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(Include.NON_NULL);

        if (result.getActualResult() instanceof BeanCreationException) {
            Message message = constructionBeanCreateMessage();
            String json = mapper.writeValueAsString(message);
            response.setContentLength(json.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().print(json);
            return;
        }

        String json = mapper.writeValueAsString(result.getActualResult());
        log("Render JSON result with size: " + json.length());
        // log("Render JSON result: "+ json);
        response.setContentLength(json.getBytes(StandardCharsets.UTF_8).length);
        response.getWriter().print(json);

        return;

    }

    public boolean isHttps(HttpServletRequest pRequest) {
        String proto = pRequest.getHeader("X-Forwarded-Proto");
        if (proto != null && "https".equals(proto)) {
            return true;
        }
        return "https".equals(pRequest.getScheme());
    }

    protected Message constructionBeanCreateMessage() {
        Message message = new Message();
        message.setLevel("fatal");
        message.setSourcePosition("spring configuration xml files");
        message.setBody("Bean Creation Error, please check it from server side");

        return message;
    }

    private void log(String header) {
        // TODO Auto-generated method stub
        System.out.println(header);
    }

    protected void renderHTMLPage(HttpServlet servlet, InvocationResult result, HttpServletRequest request,
                                  HttpServletResponse response) throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html");
        response.addHeader("Cache-Control", "no-cache, must-revalidate");
        String renderClass = result.getActualResult().getClass().getName();
        if (result.getResponseHeader() != null && result.getResponseHeader().containsKey("X-Class")) {
            renderClass = result.getResponseHeader().get("X-Class");
        }
        response.addHeader("X-Env-Type", result.getEnvType());
        response.addHeader("X-Env-Name", result.getEnvName());
        response.addHeader("X-Class", renderClass);
        fillOrigin(result, request, response);
        // 其他header
        if (result.getResponseHeader() != null) {
            for (String hName : result.getResponseHeader().keySet()) {
                if ("X-Class".equals(hName)) {
                    continue;
                }
                response.addHeader(hName, result.getResponseHeader().get(hName));
            }
        }
        request.setAttribute("result", result.getActualResult());
        request.setAttribute("rootResult", result);
        request.setAttribute("baseURL", this.calcBaseURL(request));

        this.dispatchView(servlet, request, response, result);

    }

    protected void renderJavascript(HttpServlet servlet, InvocationResult result, HttpServletRequest request,
                                    HttpServletResponse response) throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/javascript");
        response.addHeader("Cache-Control", "no-cache, must-revalidate");
        response.addHeader("X-Env-Type", result.getEnvType());
        response.addHeader("X-Env-Name", result.getEnvName());
        // 其他header
        if (result.getResponseHeader() != null) {
            for (String hName : result.getResponseHeader().keySet()) {
                response.addHeader(hName, result.getResponseHeader().get(hName));
            }
        }
        ObjectMapper mapper = getObjectMapper();

        String json = mapper.writeValueAsString(result.getActualResult());
        response.setContentLength(json.getBytes(StandardCharsets.UTF_8).length);
        response.getWriter().print("var initValue=");
        response.getWriter().print(json);
        response.getWriter().print(";");

    }

    protected String joinParametersTypes(Type[] types, char connectChar) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < types.length; i++) {
            if (i > 0) {
                stringBuilder.append(connectChar);
            }

            Class clazz = (Class) types[i];
            stringBuilder.append(clazz.getSimpleName());
        }
        return stringBuilder.toString();
    }

    protected void handleArgumentExcepttion(String message) {
        throw new IllegalArgumentException(message);
    }

    protected String getGenericResultRenderKey(InvocationResult result) {

        if (result == null) {
            handleArgumentExcepttion("getRenderKey(InvocationResult result)： result should not be null.");
        }
        final InvocationContext context = result.getInvocationContext();
        if (context == null) {
            handleArgumentExcepttion(
                    "getRenderKey(InvocationResult result)： result.getInvocationContext() should not be null.");
        }
        Method method = context.getMethodToCall();

        if (method == null) {
            handleArgumentExcepttion(
                    "getRenderKey(InvocationResult result)： result.getInvocationContext().getMethodToCall() should not be null.");
        }

        if (!isGenericReturnType(method)) {
            handleArgumentExcepttion("Should not call  getRenderKey() when return type is not a parameterized type.");
        }

        Type type = method.getGenericReturnType();

        ParameterizedType parameterReutrnType = (ParameterizedType) type;
        Type[] types = parameterReutrnType.getActualTypeArguments();
        String parameterTypeExpr = joinParametersTypes(types, '_');
        String returnTypeExpr = method.getReturnType().getSimpleName();
        return parameterTypeExpr + "$" + returnTypeExpr;

        // System.out.println(method.getReturnType());

    }

    protected boolean isGenericResult(InvocationResult result) {
        if (result == null) {
            return false;
        }
        if (result.getInvocationContext() == null) {
            return false;
        }
        if (result.getActualResult() == null) {
            return false;
        }
        if (result.getActualResult() instanceof Throwable) {
            return false;
        }

        Method method = result.getInvocationContext().getMethodToCall();
        return isGenericReturnType(method);
    }

    protected boolean isArrayResult(InvocationResult result) {
        if (result == null) {
            return false;
        }
        if (result.getInvocationContext() == null) {
            return false;
        }
        if (result.getActualResult() == null) {
            return false;
        }
        Type type = result.getActualResult().getClass();
        return isArrayType(type);
    }

    protected boolean isArrayType(Type type) {

        Class typeClazz = (Class) type;
        if (typeClazz.isArray()) {
            return true;
        }
        return false;
    }

    protected boolean isGenericReturnType(Method method) {

        Type type = method.getGenericReturnType();
        if (type instanceof ParameterizedType) {
            return true;
        }

        return false;
    }

    protected void dispatchView(HttpServlet servlet, HttpServletRequest request, HttpServletResponse response,
                                InvocationResult result) throws ServletException, IOException {
        RequestDispatcher view = getRenderView(servlet, request, result);
        view.include(request, response);

    }

    protected void logInfo(String message) {
        // log.log(Level.INFO, message);
        System.out.println("Render:" + message);
    }

    protected RequestDispatcher getRenderView(HttpServlet servlet, HttpServletRequest request, InvocationResult result)
            throws MalformedURLException {
        if (isGenericResult(result)) {
            return request.getRequestDispatcher("/" + getGenericResultRenderKey(result) + ".jsp");
        }
        if (isArrayResult(result)) {
            return request.getRequestDispatcher("/" + getArrayResultRenderKey(result) + ".jsp");
        }
        return getSimpleRenderView(servlet, request, result.getActualResult());

    }

    protected String getArrayResultRenderKey(InvocationResult result) {
        if (!isArrayResult(result)) {
            throw new IllegalArgumentException("The result should be an array to be processed");
        }
        // The method isArrayResult will check the result, result.getActualResult()
        Class componentType = result.getActualResult().getClass().getComponentType();

        return componentType.getName() + "$Array";
    }

    private Map<Class, String> viewCache = new ConcurrentHashMap<Class, String>();

    protected boolean isProduction() {
        return "PRODUDCTION".equals(System.getenv("DEPLOYMOD"));
        // return true;
    }

    // Referer:http://localhost:8080/naf/navi/index/decorationCountryCenterManager/
    protected String[] getViewLayers(HttpServletRequest request) {

        if (1 == 1) {
            return new String[]{"customview", "coreview", "cafview"};
        }
        if (1 == 1) {
            return new String[]{"coreview", "cafview"};
        }

        String reqURI = request.getRequestURI();
        String referalURL = request.getHeader("Referer");
        if (reqURI.indexOf("/navi/index/") > 0) {/// navi/index/
            if (referalURL == null) {
                return new String[]{"coreview", "cafview"};
            }
            if (referalURL.indexOf("/navi/index/") > 0) {
                return new String[]{"coreview", "cafview"};
            }

        }

        //
        String ajax = request.getHeader("X-Requested-With");

        if (referalURL == null) {
            // no refer, it means it is the start page
            return new String[]{"customview", "coreview", "cafview"};
        }
        if (referalURL.indexOf("/navi/index/") > 0 && ajax != null) {/// navi/index/
            // this is an AJAX load from management console
            return new String[]{"coreview", "cafview"};
        }
        return new String[]{"customview", "coreview", "cafview"};

    }

    protected RequestDispatcher getSimpleRenderView(HttpServlet servlet, HttpServletRequest request, Object object)
            throws MalformedURLException {

        Class temp = object.getClass();

        if (isProduction()) {
            // 如果是生产环境，就使用缓存， 方便调试的时候随时修改视图
            String cachedPage = viewCache.get(temp);
            if (cachedPage != null) {
                logInfo("found cache for " + cachedPage);
                return request.getRequestDispatcher(cachedPage);
            }
        }

        // 这个代码是根据类信息找到一个合适的渲染视图，如果找不到相应的视图，
        // 就从尝试从父类中找到一个视图
        // 通过一个循环实现了递归调用，节约了栈空间, 为什么节约了栈空间呢，因为一个多一次层次的调用就会多一次栈分配过程
        ServletContext context = servlet.getServletContext();
        String folderList[] = getViewLayers(request);
        temp = object.getClass();
        while (temp != null) {
            for (String folderName : folderList) {

                String jsp = "/" + folderName + "/" + getFileNameFromResult(temp,object) + ".jsp";
                // logInfo("trying to find: " + jsp);
                URL url = context.getResource(jsp);
                if (url != null) {
                    viewCache.put(temp, jsp);
                    logInfo("\tluck to find: " + jsp);
                    return request.getRequestDispatcher(jsp);
                }
                logInfo("\ttried to find: " + jsp + ", will try next....");

            }
            temp = temp.getSuperclass();
        }
        final String defaultView = "/coreview/java/lang/Object.jsp";
        logInfo("using default view: " + defaultView);
        return request.getRequestDispatcher(defaultView);// should
        // not
        // go
        // here

    }

    protected String getFileNameFromClass(Class<?> clazz) {
        String name = clazz.getName();
        String segs[] = name.split("\\.");
        return join(segs, segs.length, "/");

    }
    protected String getFileNameFromResult(Class<?> clazz,Object object) {


        if(!(object instanceof CafEntity)){

            return getFileNameFromClass(clazz);
        }
        CafEntity cafEntity=(CafEntity)object;

        if(cafEntity.viewSuffix()==null||cafEntity.viewSuffix().isEmpty()){
            return getFileNameFromClass(clazz);
        }
        return String.join(".",getFileNameFromClass(clazz),cafEntity.viewSuffix());




    }

    protected String join(String[] array, int end, String joinStr) {
        if (end > array.length) {
            throw new IllegalArgumentException("end is bigger than length");

        }
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < end; i++) {
            if (i > 0) {
                stringBuilder.append(joinStr);
            }
            stringBuilder.append(array[i]);
        }
        return stringBuilder.toString();
    }

}
